﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Leow.TextEditor
{
    /// <summary>
    /// Represents the line number margin of the <c>TextEditor</c> control.
    /// </summary>
    internal class LineNumberMargin : FrameworkElement
    {
        private Rect _backgroundRect;
        private double _contentWidth;
        private readonly RectangleGeometry _foregroundClip;
        private int _lineCount;
        private readonly TextEditor _owner;
        private readonly List<double> _widths;
        private Point _separatorPoint0;
        private Point _separatorPoint1;

        /// <summary>
        /// Initializes a new instance of the <c>LineNumberMargin</c> class.
        /// </summary>
        /// <param name="owner">The <c>TextEditor</c> control in which this
        /// <c>LineNumberMargin</c> will be placed.</param>
        public LineNumberMargin(TextEditor owner)
        {
            _owner = owner;

            var ownerGrid = _owner
                .GetFirstVisualChildOfType<Grid>();
            ownerGrid.ColumnDefinitions.Insert(0, new ColumnDefinition
            {
                Width = GridLength.Auto
            });
            foreach (UIElement child in ownerGrid.Children)
            {
                Grid.SetColumn(child, Grid.GetColumn(child) + 1);
            }

            ownerGrid.Children.Add(this);

            _widths = new List<double>();
            UpdateWidth();

            _foregroundClip = new RectangleGeometry();
            SeparatorPen = new Pen(_owner.SeparatorBrush, _owner.SeparatorWidth);
        }

        /// <summary>
        /// Gets or sets the pen that is used to draw the separator between the
        /// line number margin and the text content area.
        /// </summary>
        public Pen SeparatorPen { get; private set; }

        /// <summary>
        /// Removes this line number margin from the owning <c>TextEditor</c>
        /// control.
        /// </summary>
        public void Close()
        {
            var ownerGrid = _owner
                .GetFirstVisualChildOfType<Grid>();
            ownerGrid.Children.Remove(this);
            foreach (UIElement child in ownerGrid.Children)
            {
                Grid.SetColumn(child, Grid.GetColumn(child) - 1);
            }

            ownerGrid.ColumnDefinitions.RemoveAt(0);
        }

        /// <summary>
        /// Renders this line number margin on the <c>TextEditor</c> surface.
        /// </summary>
        /// <param name="drawingContext">The drawing instructions for a specific
        /// element. This context is provided to the layout system.</param>
        /// <param name="firstVisibleLineIndex">The index of the first visible
        /// text line.</param>
        /// <param name="lastVisibleLineIndex">The index of the last visible
        /// text line.</param>
        /// <param name="firstVisibleCharLeadingEdge">Location of the leading
        /// edge of the first visible character.</param>
        public void Render(
            DrawingContext drawingContext,
            int firstVisibleLineIndex,
            int lastVisibleLineIndex,
            Point firstVisibleCharLeadingEdge)
        {
            drawingContext.DrawRectangle(
                _owner.LineNumberMarginBackground,
                null,
                _backgroundRect);
            drawingContext.DrawLine(
                SeparatorPen,
                _separatorPoint0,
                _separatorPoint1);

            string visibleLineNumberMarginText = GetVisibleLineNumberMarginText(
                firstVisibleLineIndex,
                lastVisibleLineIndex);
            var typeface = new Typeface(
                _owner.FontFamily,
                _owner.FontStyle,
                _owner.FontWeight,
                _owner.FontStretch);
            var lineNumberMarginFormattedText = new FormattedText(
                visibleLineNumberMarginText,
                CultureInfo.CurrentCulture,
                FlowDirection.LeftToRight,
                typeface,
                _owner.FontSize,
                _owner.LineNumberMarginForeground,
                null,
                TextOptions.GetTextFormattingMode(this))
            {
                TextAlignment = TextAlignment.Right,
                Trimming = TextTrimming.None
            };

            drawingContext.PushClip(_foregroundClip);
            Point lineNumberMarginPosition = TranslatePoint(new Point(), _owner);
            var lineNumberMarginTextPosition = new Point(
                lineNumberMarginPosition.X + _owner.LineNumberMarginPadding.Left + _contentWidth,
                firstVisibleCharLeadingEdge.Y);
            drawingContext.DrawText(
                lineNumberMarginFormattedText,
                lineNumberMarginTextPosition);
            drawingContext.Pop();
        }

        /// <summary>
        /// Recalculate certain drawing parameters that will be used by the
        /// renderer to draw this line number margin.
        /// </summary>
        /// <param name="ownerScrollContentPresenter">The
        /// <c>ScrollContentPresenter</c> in the owning <c>TextEditor</c>.
        /// </param>
        /// <param name="ownerForegroundClip">The owning <c>TextEditor</c>'s
        /// foreground clip region.</param>
        public void UpdateDrawingParams(
            ScrollContentPresenter ownerScrollContentPresenter,
            RectangleGeometry ownerForegroundClip)
        {
            Point scrollContentPresenterPosition
                = ownerScrollContentPresenter.TranslatePoint(new Point(), _owner);

            double backgroundWidth
                = scrollContentPresenterPosition.X - ownerScrollContentPresenter.Margin.Left - _owner.BorderThickness.Left;
            double backgroundHeight
                = _owner.ActualHeight - _owner.BorderThickness.Top - _owner.BorderThickness.Bottom;
            _backgroundRect = new Rect(
                _owner.BorderThickness.Left,
                _owner.BorderThickness.Top,
                Math.Max(backgroundWidth, 0),
                Math.Max(backgroundHeight, 0));

            Point lineNumberMarginPosition = TranslatePoint(new Point(), _owner);
            var lineNumberMarginForegroundRect = new Rect(
                lineNumberMarginPosition.X + _owner.LineNumberMarginPadding.Left,
                ownerForegroundClip.Rect.Top,
                _contentWidth,
                ownerForegroundClip.Rect.Height);
            _foregroundClip.Rect = lineNumberMarginForegroundRect;

            double x = Math.Floor(_backgroundRect.Left + _backgroundRect.Width - _owner.SeparatorWidth / 2) + 0.5;
            _separatorPoint0 = new Point(x, _backgroundRect.Top);
            _separatorPoint1 = new Point(x, _backgroundRect.Bottom);
        }

        /// <summary>
        /// Updates the width of this line number margin to fit the line number
        /// with the longest measured width.
        /// </summary>
        /// <param name="isUseCoercion"><c>true</c> to update the width despite
        /// not having changes; <c>false</c> to ignore the update of the width
        /// if there were no changes.</param>
        /// <param name="isCalculateFromScratch">A value to indicate whether
        /// this method should recalculate the widths of all line numbers from
        /// scratch, or just the line numbers that have been newly added or
        /// removed since the last call of this method.</param>
        public void UpdateWidth(
            bool isUseCoercion = false,
            bool isCalculateFromScratch = false)
        {
            // _owner.LineCount was found to be unreliable sometimes, returning
            // zero when there are definitely some texts in the TextEditor
            int ownerLineCount = _owner.LineTexts.Count;

            if (isCalculateFromScratch)
            {
                _widths.Clear();

                var typeface = new Typeface(
                    _owner.FontFamily,
                    _owner.FontStyle,
                    _owner.FontWeight,
                    _owner.FontStretch);
                TextFormattingMode textFormattingMode
                    = TextOptions.GetTextFormattingMode(_owner);

                int firstLineNumber = _owner.LineNumberMarginStartingNumber;
                int lastLineNumber
                    = ownerLineCount + _owner.LineNumberMarginStartingNumber;

                for (int i = firstLineNumber; i < lastLineNumber; i++)
                {
                    var ft = new FormattedText(
                        i.ToString(),
                        CultureInfo.CurrentCulture,
                        FlowDirection.LeftToRight,
                        typeface,
                        _owner.FontSize,
                        null,
                        null,
                        textFormattingMode)
                    {
                        Trimming = TextTrimming.None
                    };

                    _widths.Add(ft.Width);
                }
            }
            else if (ownerLineCount > _lineCount)
            {
                var typeface = new Typeface(
                    _owner.FontFamily,
                    _owner.FontStyle,
                    _owner.FontWeight,
                    _owner.FontStretch);
                TextFormattingMode textFormattingMode
                    = TextOptions.GetTextFormattingMode(_owner);

                int firstLineNumber
                    = _lineCount + _owner.LineNumberMarginStartingNumber;
                int lastLineNumber
                    = ownerLineCount + _owner.LineNumberMarginStartingNumber;

                for (int i = firstLineNumber; i < lastLineNumber; i++)
                {
                    var ft = new FormattedText(
                        i.ToString(),
                        CultureInfo.CurrentCulture,
                        FlowDirection.LeftToRight,
                        typeface,
                        _owner.FontSize,
                        null,
                        null,
                        textFormattingMode)
                    {
                        Trimming = TextTrimming.None
                    };

                    _widths.Add(ft.Width);
                }
            }
            else if (ownerLineCount < _lineCount)
            {
                _widths.RemoveRange(
                    ownerLineCount,
                    _widths.Count - ownerLineCount);
            }
            else if (!isUseCoercion)
            {
                return;
            }

            _lineCount = ownerLineCount;
            _contentWidth = _widths
                .Max();
            Width
                = _owner.LineNumberMarginPadding.Left
                + _contentWidth
                + _owner.LineNumberMarginPadding.Right
                + _owner.SeparatorWidth;
            UpdateLayout();
        }

        /// <summary>
        /// <para>Estimates and returns the portion of the text that is visible
        /// to the user.</para>
        /// <para>The renderer will then not need to draw the entire text and
        /// this might improve the performance.</para>
        /// </summary>
        /// <param name="firstVisibleLineIndex">The index of the first visible
        /// text line.</param>
        /// <param name="lastVisibleLineIndex">The index of the last visible
        /// text line.</param>
        /// <returns>The estimated visible text to be rendered.</returns>
        private string GetVisibleLineNumberMarginText(
            int firstVisibleLineIndex,
            int lastVisibleLineIndex)
        {
            int firstLineNumber
                = firstVisibleLineIndex + _owner.LineNumberMarginStartingNumber;
            int lastLineNumber
                = lastVisibleLineIndex + _owner.LineNumberMarginStartingNumber;

            var sb = new StringBuilder();

            for (int i = firstLineNumber; i < lastLineNumber; i++)
            {
                sb.AppendLine(i.ToString());
            }

            sb.Append(lastLineNumber);
            return sb.ToString();
        }
    }
}
